package Utils.excel;

import Utils.converters.Data2PoiDataType;
import Utils.converters.DataType;
import Utils.converters.MyUtils;
import Utils.delegations.IFunc;
import Utils.reflect.ReflectUtils;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;

/**
 * POI工具类
 *
 * @author d11yu
 */
public class PoiUtils {
    //region 基本方法

    /**
     * 获取Excel单元格的内容（不用管是字符串，还是数值），返回字符串。
     *
     * @param cell 待处理的单元格
     * @return
     */
    public static String getStringCellValue(Cell cell) {
        try {
            return cell.getStringCellValue();
        } catch (Exception e) {
            return cell.getNumericCellValue() + "";
        }
    }

    /**
     * 获取Excel单元格的内容（不用管是字符串，还是数值），返回Object。
     *
     * @param cell 待处理的单元格
     * @return
     */
    public static Object getCellValue(Cell cell) {
        try {
            return cell.getStringCellValue();
        } catch (Exception e) {
            return cell.getNumericCellValue();
        }
    }

    /**
     * 从Excel行中找到跟目标字符串匹配的单元格，并返回单元格的序号（0-based）。如果未找到，则返回-1。
     *
     * @param row     待匹配的行
     * @param content 目标字符串内容
     * @return
     */
    public static int matchCellFromRow(Row row, String content) {
        short lastCellNum = row.getLastCellNum();
        for (int i = 0; i < lastCellNum; i++) {
            if (getStringCellValue(row.getCell(i)).equals(content)) {
                return i;
            }
        }
        return -1;
    }
    //endregion

    //region 读取Excel工作表

    /**
     * 从Excel工作表获取词条列表，返回的对象的所有字段均为字符串类型。
     *
     * @param clazz     Java bean的类
     * @param sht       Excel工作表
     * @param colNames  列头数组
     * @param colLabels Java bean的字段，注意要和colNames一一对应。
     * @return
     */
    public static <T> ArrayList<T> getEntryList(Class<T> clazz, Sheet sht, String[] colNames, String[] colLabels) {
        ArrayList<T> res = new ArrayList<>();
        try {
            int lastRowNum = sht.getLastRowNum();
            int[] colIndexes = new int[colNames.length];
            // 列名mapping
            Row headRow = sht.getRow(0);
            for (int i = 0; i < colIndexes.length; i++) {
                colIndexes[i] = matchCellFromRow(headRow, colNames[i]);
            }

            for (int i = 1; i <= lastRowNum; i++) {
                T t = clazz.newInstance();
                Row row = sht.getRow(i);
                for (int j = 0; j < colLabels.length; j++) {
                    ReflectUtils.setDeclaredField(t, colLabels[j], getStringCellValue(row.getCell(colIndexes[j])));
                }
                res.add(t);
            }
            return res;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 从Excel工作表获取词条列表。
     *
     * @param clazz      Java bean的类
     * @param sht        Excel工作表
     * @param colNames   列头数组
     * @param colLabels  Java bean的字段，注意要和colNames一一对应。
     * @param converters 转换器数组，将字符串转换成Java bean字段类型，注意要一一对应。
     * @param <T>        Java bean
     * @return
     */
    public static <T> ArrayList<T> getEntryList(Class<T> clazz, Sheet sht, String[] colNames, String[] colLabels, IFunc[] converters) {
        ArrayList<T> res = new ArrayList<>();
        try {
            int lastRowNum = sht.getLastRowNum();
            int[] colIndexes = new int[colNames.length];
            // 列名mapping
            Row headRow = sht.getRow(0);
            for (int i = 0; i < colIndexes.length; i++) {
                colIndexes[i] = matchCellFromRow(headRow, colNames[i]);
            }

            for (int i = 1; i <= lastRowNum; i++) {
                T t = clazz.newInstance();
                Row row = sht.getRow(i);
                for (int j = 0; j < colLabels.length; j++) {
                    Object value = converters[j].invoke(getStringCellValue(row.getCell(colIndexes[j])));
                    ReflectUtils.setDeclaredField(t, colLabels[j], value);
                }
                res.add(t);
            }
            return res;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 从Excel工作表获取词条列表。（默认获取第一张工作表的数据）
     *
     * @param clazz      Java bean的类
     * @param path       Excel工作簿路径
     * @param colLabels  Java bean的字段，注意是从0开始的连续列。
     * @param converters 转换器数组，将字符串转换成Java bean字段类型，注意要一一对应。
     * @return
     */
    public static <T> ArrayList<T> getEntryList(Class<T> clazz, String path, String[] colLabels, IFunc[] converters) throws IOException {
        Workbook wb = null;
        try (FileInputStream is = new FileInputStream(path)) {
            wb = new XSSFWorkbook(is);
            return getEntryList(clazz, wb.getSheetAt(0), colLabels, converters);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (wb != null) {
                wb.close();
            }
        }
        return null;
    }

    /**
     * 从Excel工作表获取词条列表。（默认获取第一张工作表的数据）
     *
     * @param clazz      Java bean的类
     * @param path       Excel工作簿路径
     * @param colIndexes 列头序号数组（0-based）
     * @param colLabels  Java bean的字段，注意要和colIndexes一一对应。
     * @param converters 转换器数组，将字符串转换成Java bean字段类型，注意要一一对应。
     * @return
     */
    public static <T> ArrayList<T> getEntryList(Class<T> clazz, String path, int[] colIndexes, String[] colLabels, IFunc[] converters) throws IOException {
        return getEntryList(clazz, path, 0, colIndexes, colLabels, converters);
    }

    /**
     * 从Excel工作表获取词条列表。
     *
     * @param clazz      Java bean的类
     * @param path       Excel工作簿路径
     * @param shtIndex   Excel工作表的序号（0-based）
     * @param colIndexes 列头序号数组（0-based）
     * @param colLabels  Java bean的字段，注意要和colIndexes一一对应。
     * @param converters 转换器数组，将字符串转换成Java bean字段类型，注意要一一对应。
     * @return
     */
    public static <T> ArrayList<T> getEntryList(Class<T> clazz, String path, int shtIndex, int[] colIndexes, String[] colLabels, IFunc[] converters) throws IOException {
        Workbook wb = null;
        try (FileInputStream is = new FileInputStream(path)) {
            wb = new XSSFWorkbook(is);
            return getEntryList(clazz, wb.getSheetAt(shtIndex), colIndexes, colLabels, converters);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (wb != null) {
                wb.close();
            }
        }
        return null;
    }

    /**
     * 从Excel工作表获取词条列表。
     *
     * @param clazz      Java bean的类
     * @param path       Excel工作簿路径
     * @param shtName    Excel工作表的名称
     * @param colIndexes 列头序号数组（0-based）
     * @param colLabels  Java bean的字段，注意要和colIndexes一一对应。
     * @param converters 转换器数组，将字符串转换成Java bean字段类型，注意要一一对应。
     * @return
     */
    public static <T> ArrayList<T> getEntryList(Class<T> clazz, String path, String shtName, int[] colIndexes, String[] colLabels, IFunc[] converters) throws IOException {
        Workbook wb = null;
        try (FileInputStream is = new FileInputStream(path)) {
            wb = new XSSFWorkbook(is);
            return getEntryList(clazz, wb.getSheet(shtName), colIndexes, colLabels, converters);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (wb != null) {
                wb.close();
            }
        }
        return null;
    }

    /**
     * 从Excel工作表获取词条列表。
     *
     * @param clazz      Java bean的类
     * @param sht        Excel工作表
     * @param colIndexes 列头序号数组（0-based）
     * @param colLabels  Java bean的字段，注意要和colIndexes一一对应。
     * @param converters 转换器数组，将字符串转换成Java bean字段类型，注意要一一对应。
     * @param <T>        Java bean
     * @return
     */
    public static <T> ArrayList<T> getEntryList(Class<T> clazz, Sheet sht, int[] colIndexes, String[] colLabels, IFunc[] converters) {
        ArrayList<T> res = new ArrayList<>();
        try {
            int lastRowNum = sht.getLastRowNum();

            for (int i = 1; i <= lastRowNum; i++) {
                T t = clazz.newInstance();
                Row row = sht.getRow(i);
                for (int j = 0; j < colLabels.length; j++) {
                    Object value = converters[j].invoke(getStringCellValue(row.getCell(colIndexes[j])));
                    ReflectUtils.setDeclaredField(t, colLabels[j], value);
                }
                res.add(t);
            }
            return res;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 从Excel工作表获取词条列表。
     *
     * @param clazz      Java bean的类
     * @param sht        Excel工作表
     * @param colLabels  Java bean的字段，注意是从第0列开始的连续列。
     * @param converters 转换器数组，将字符串转换成Java bean字段类型，注意要一一对应。
     * @param <T>        Java bean
     * @return
     */
    public static <T> ArrayList<T> getEntryList(Class<T> clazz, Sheet sht, String[] colLabels, IFunc[] converters) {
        ArrayList<T> res = new ArrayList<>();
        try {
            int lastRowNum = sht.getLastRowNum();

            for (int i = 1; i <= lastRowNum; i++) {
                T t = clazz.newInstance();
                Row row = sht.getRow(i);
                for (int j = 0; j < colLabels.length; j++) {
                    Object value = converters[j].invoke(getStringCellValue(row.getCell(j)));
                    ReflectUtils.setDeclaredField(t, colLabels[j], value);
                }
                res.add(t);
            }
            return res;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 根据InputStream打开Workbook
     *
     * @param is
     * @return
     * @throws IOException
     */
    public static Workbook openWorkbookFromPath(InputStream is) throws IOException {
        return new XSSFWorkbook(is);
    }
    //endregion

    //region 写入Excel工作表

    /**
     * 将数据写入工作表，可以指定日期的输出格式。
     *
     * @param list      待写入的数据
     * @param sht       待写入的工作表
     * @param colLabels Java bean的字段名数组
     * @param converter 数据类型转换器，可以指定日期的输出格式
     * @param colWidths 工作表各列宽度，单位为1个标准字符的宽度。如果为null，则不设置宽度。
     * @param strDate   是否将日期以字符串的形式输出。
     * @throws Exception
     */
    public static <T> void entryList2Sht(List<T> list, Sheet sht, String[] colLabels, Data2PoiDataType converter, int[] colWidths, boolean strDate) throws Exception {
        entryList2Sht(list, sht, colLabels, colLabels, converter, colWidths, strDate);
    }

    /**
     * 将数据写入工作表，默认以"yyyy-MM-dd"的格式输出日期。
     *
     * @param clazz     T的class
     * @param list      待写入的数据
     * @param sht       待写入的工作表
     * @param colWidths 工作表各列宽度，单位为1个标准字符的宽度。如果为null，则不设置宽度。
     * @param strDate   是否将日期以字符串的形式输出。
     * @throws Exception
     */
    public static <T> void entryList2Sht(Class<T> clazz, List<T> list, Sheet sht, int[] colWidths, boolean strDate) throws Exception {
        Field[] fields = ReflectUtils.getDeclaredFields(clazz);
        String[] cols = new String[fields.length];
        for (int i = 0; i < cols.length; i++) {
            cols[i] = fields[i].getName();
        }
        entryList2Sht(list, sht, cols, cols, colWidths, strDate);
    }

    /**
     * 将数据写入工作表，默认以"yyyy-MM-dd"的格式输出日期。
     *
     * @param list      待写入的数据
     * @param sht       待写入的工作表
     * @param colLabels Java bean的字段名数组
     * @param colWidths 工作表各列宽度，单位为1个标准字符的宽度。如果为null，则不设置宽度。
     * @param strDate   是否将日期以字符串的形式输出。
     * @throws Exception
     */
    public static <T> void entryList2Sht(List<T> list, Sheet sht, String[] colLabels, int[] colWidths, boolean strDate) throws Exception {
        entryList2Sht(list, sht, colLabels, colLabels, colWidths, strDate);
    }

    /**
     * 将数据写入工作表，默认以"yyyy-MM-dd"的格式输出日期。
     *
     * @param list      待写入的数据
     * @param sht       待写入的工作表
     * @param colLabels Java bean的字段名数组
     * @param colNames  工作表首行字段数组，注意要与colLabels一一对应。
     * @param colWidths 工作表各列宽度，单位为1个标准字符的宽度。如果为null，则不设置宽度。
     * @param strDate   是否将日期以字符串的形式输出。
     * @throws Exception
     */
    public static <T> void entryList2Sht(List<T> list, Sheet sht, String[] colLabels, String[] colNames, int[] colWidths, boolean strDate) throws Exception {
        entryList2Sht(list, sht, colLabels, colNames, new Data2PoiDataType(), colWidths, strDate);
    }

    /**
     * 将数据写入工作表，可以指定日期的输出格式
     *
     * @param list      待写入的数据
     * @param sht       待写入的工作表
     * @param colLabels Java bean的字段名数组
     * @param colNames  工作表首行字段数组，注意要与colLabels一一对应。
     * @param converter 数据类型转换器，可以指定日期的输出格式
     * @param colWidths 工作表各列宽度，单位为1个标准字符的宽度。如果为null，则不设置宽度。
     * @param strDate   是否将日期以字符串的形式输出。
     * @param <T>       Java bean类型
     * @throws Exception
     */
    public static <T> void entryList2Sht(List<T> list, Sheet sht, String[] colLabels, String[] colNames, Data2PoiDataType converter, int[] colWidths, boolean strDate) throws Exception {
        // 填写headers
        Row headerRow = sht.createRow(0);
        for (int i = 0; i < colNames.length; i++) {
            headerRow.createCell(i).setCellValue(colNames[i]);
        }

        // 设置各列宽度
        if (colWidths != null) {
            for (int i = 0; i < colWidths.length; i++) {
                sht.setColumnWidth(i, colWidths[i] * 256);
            }
        }

        // 填写内容
        for (int i = 0; i < list.size(); i++) {
            T t = list.get(i);
            Row row = sht.createRow(i + 1);
            for (int j = 0; j < colLabels.length; j++) {
                Object value = ReflectUtils.getDeclaredFieldValue(t, colLabels[j]);
                DataType dataType = MyUtils.getDataType(value);
                switch (dataType) {
                    case BOOLEAN: {
                        row.createCell(j).setCellValue(converter.invoke(Boolean.class, value));
                        break;
                    }
                    case DATE_UTIL: {
                        if (strDate) {
                            row.createCell(j).setCellValue(converter.invoke(String.class, value));
                        } else {
                            row.createCell(j).setCellValue(converter.invoke(Date.class, value));
                        }
                        break;
                    }
                    case DATE_SQL: {
                        if (strDate) {
                            row.createCell(j).setCellValue(converter.invoke(String.class, value));
                        } else {
                            row.createCell(j).setCellValue(converter.invoke(java.sql.Date.class, value));
                        }
                        break;
                    }
                    case DOUBLE: {
                        row.createCell(j).setCellValue(converter.invoke(Double.class, value));
                        break;
                    }
                    case INT: {
                        row.createCell(j).setCellValue(converter.invoke(Integer.class, value));
                        break;
                    }
                    case STRING: {
                        row.createCell(j).setCellValue(converter.invoke(String.class, value));
                        break;
                    }
                    default: {
                        row.createCell(j).setCellValue("");
                        break;
                    }
                }
            }
        }
    }

    /**
     * 将数据写入工作表，可以指定日期的输出格式，并保存到工作簿。
     *
     * @param list     待写入的数据
     * @param path     工作簿的保存路径
     * @param colWidth 列宽
     * @param strDate  是否将日期以字符串的形式输出。
     * @throws Exception
     */
    public static <T> void entryList2Xlsx(List<T> list, String path, Integer colWidth, boolean strDate) throws Exception {
        String[] fdNames = ReflectUtils.getDeclaredFieldNames(list.get(0));
        int[] colWidths = new int[fdNames.length];
        Arrays.fill(colWidths, colWidth);
        entryList2Xlsx(list, path, fdNames, fdNames, new Data2PoiDataType(), colWidths, strDate);
    }

    /**
     * 将数据写入工作表，可以指定日期的输出格式，并保存到工作簿。
     *
     * @param list      待写入的数据
     * @param path      工作簿的保存路径
     * @param colWidths 工作表各列宽度，单位为1个标准字符的宽度。如果为null，则不设置宽度。
     * @param strDate   是否将日期以字符串的形式输出。
     * @throws Exception
     */
    public static <T> void entryList2Xlsx(List<T> list, String path, int[] colWidths, boolean strDate) throws Exception {
        String[] fdNames = ReflectUtils.getDeclaredFieldNames(list.get(0));
        entryList2Xlsx(list, path, fdNames, fdNames, new Data2PoiDataType(), colWidths, strDate);
    }

    /**
     * 将数据写入工作表，可以指定日期的输出格式，并保存到工作簿。
     *
     * @param list      待写入的数据
     * @param path      工作簿的保存路径
     * @param colLabels Java bean的字段名数组
     * @param colNames  工作表首行字段数组，注意要与colLabels一一对应。
     * @param converter 数据类型转换器，可以指定日期的输出格式
     * @param colWidths 工作表各列宽度，单位为1个标准字符的宽度。如果为null，则不设置宽度。
     * @param strDate   是否将日期以字符串的形式输出。
     * @param <T>       Java bean类型
     * @throws Exception
     */
    public static <T> void entryList2Xlsx(List<T> list, String path, String[] colLabels, String[] colNames, Data2PoiDataType converter, int[] colWidths, boolean strDate) throws Exception {
        try (Workbook wb = new XSSFWorkbook();
             FileOutputStream stream = new FileOutputStream(path)) {
            Sheet sht = wb.createSheet();
            entryList2Sht(list, sht, colLabels, colNames, converter, colWidths, strDate);
            wb.write(stream);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    //endregion
}